import java.io.File;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Iterator;
import java.util.logging.Level;

import org.diy.traffic.config.DIYUKCity;
import org.diy.traffic.config.TrafficUKConfig;
//import org.diy.traffic.config.TrafficConfig.TrafficConfigException;
import org.diy.traffic.config.TrafficUKConfig.TrafficUKConfigException;
import org.diy.traffic.daemon.AirTrafficController;
import org.diy.traffic.sinks.TwitterDMSink;
import org.diy.traffic.sinks.TwitterStatusSink;
import org.diy.traffic.sources.BlueGhostTrafficSource;
import org.diy.traffic.sources.TwitterDMSource;
import org.diy.traffic.util.TLog;


/**
 * DIYTrafficBot v0.8_2
 * 
 * This is the main class that is invoked in the runnable .jar
 * 
 * It is responsible for reading in the configuration file, creating an Air Traffic Controller (ATC)
 * and setting up all the sources and sinks with the ATC.
 * 
 * Unfortunately, all the useful arguments have not been completely parameterized yet (i.e they're not
 * settable from the config file, must be changed in code). Below is a list of the important values 
 * you may care about, and may want to tweak before running the bot.
 * 
 * @author Dan Greenblatt
 *
 */
public class DIYTrafficBotUK {	
	
	/** 
	 * A NOTE ABOUT PARAMETERS WHICH AFFECT HOW DIYTRAFFICBOT WORKS
	 * 
	 * Some parameters are configurable from within the config file:
	 *   - how often a city should check it's Yahoo! traffic feed (a number in minutes)
	 *   - the maximum number of tweets which can be posted in one minute as status updates to a twitter feed
	 * 
	 * But there are some other IMPORTANT DEFAULT VALUES not configurable from the config file, which
	 * you may want to consider tweaking before running the bot. 
	 */
	
	/**
	 * LOG THRESHOLD
	 * Only show log entries that are at a particular detail level and above. 
	 * Defaults to Level.CONFIG, but can be changed from the command line 
	*/
	private final static Level DEFAULT_LOG_LEVEL = Level.CONFIG; //info
	private static Level useLogLevel = DEFAULT_LOG_LEVEL;
	
	private static final String helpString = "===== DIYtraffic Bot Help =====\n" +
								"Usage:\n" +
								"    % java -jar DIYtrafficBot.jar [-v | -vv | -q] [-s incidentSeverityLevel] [-c configFilePath]\n" +
								"\n" +
								"Some level of debug/status information is by default output to the terminal and mirrored \n" + 
								"to a file called 'diytraffic.log'. The following flags let you change the level \n" +
								"of debugging:\n" +
								"-v  output verbose debugging\n" +
								"-vv output *very* verbose debugging\n" +
								"-q  be quiet; minimal amount of status info\n" +
								"\n" +
								"By default, DIYtrafficBot looks for a config file called 'diytraffic.config',\n"+
								"but you can supply a different location using the -c flag:\n" +
								"-c /path/to/config/file\n" +
								"\n" +
								"By default, only traffic incident of severity level 2 or higher are reported, \n"+
								"but this value can be changed using the -s flag:\n" +
								"-s severity\n" +
								"where 'severity' is a value between 1 and 5 with 1 being the least severe and five\n"+
								"being the most severe. A lower number will result in more updates being generated\n" +
								"\n";
	
	
	/**
	 * Use this config path (./diytraffic.config) unless the user supplies another config file
	 */
	private final static String DEFAULT_CONFIG_PATH = "diytraffic.config";
	private static String useConfigPath = DEFAULT_CONFIG_PATH;
	
	/**
	 * INPUT / OUTPUT DAEMON INTERVALS
	 * These numbers (in seconds) define the interval at which the input and output daemons, respectively,
	 * 'wake up' to do their thing. Effectively, these intervals specify the lower-bound for which any
	 * components (sources, in the case of inputs and sinks, in the case outputs) can be checked. I.e. it's
	 * impossible for a source to be checked every second if the input daemon is only waking up every ten seconds.
	 */
	private final static int   DEFAULT_INPUTD_INTERVAL  = 10;
	private final static int   DEFAULT_OUTPUTD_INTERVAL = 10;
	
	/**
	 * DIRECT MESSAGE CHECKING INTERVAL
	 * This is the amount of time (in seconds) that the TwitterDMSource will check it's Twitter account for
	 * new direct messages. This should be relatively short, as it is important that, when a user send a DM
	 * with a query for traffic on a particular street, they get a response in a timely fashion.
	 */
	private final static int DEFAULT_DM_CHECK_INTERVAL = 30;
	
	/**
	 * TRAFFIC SOURCE CHECKING INTERVAL
	 * This is the amount of time (in seconds) that defines the interval on which the YahooTrafficSource will check for new traffic updates.
	 * 900 seconds == 15 minutes.
	 */
	private final static int DEFAULT_TRAFFIC_CHECK_INTERVAL = 900;
	
	/**
	 * MAX NUMBER OF DIRECT MESSAGES SENT PER MINUTE
	 * This represents the maximum number of Direct Messages a Twitter account can send (i.e. in response to
	 * queries from users) per minute. Because it's on a per-account basis (and not on an per-message-to-a-user
	 * basis, it should be fairly high, to accomodate multiple people querying the system simultaneously
	 */
	private final static int DEFAULT_DM_MAX_UPDATES = 100;
	
	/**
	 * MAX NUMBER OF STATUS UPDATES SENT PER MINUTE
	 * This represents the maximum number of status updates which can be posted to a Twitter account per minute. 
	 */
	private final static int DEFAULT_STATUS_MAX_UPDATES = 5;
	
	
	
	private final static String REMOTE_CONFIG_URI = "http://diycity.org/wiki/index.php?title=Sample_config_file&action=raw";
	
	/**
	 * This is an amount of time, in seconds, which represents an interval on which the DIYtraffic Bot will look
	 * for updates to its configuration file.
	 */
	private final static int DEFAULT_CONFIG_UPDATE_INTERVAL = 3600;
	
	public static TrafficUKConfig trafficConfig = new TrafficUKConfig();
	
	public static void main(String[] args) throws Exception {
		
		// Set the location for the log file (which will contain debugging info, etc.)
		TLog.setLogFilename("diytraffic.log");
		TLog.setLogThreshold(DEFAULT_LOG_LEVEL);
		
		for (int i=0; i<args.length; i++) {
			if ( ((String)args[i]).contains("help") || (((String)args[i]).compareTo("-h") == 0) ) {
				System.out.println(helpString);
				System.exit(1);
			}
			else if ( ((String)args[i]).compareTo("-v") == 0) {
				useLogLevel = Level.FINE;
			} else if (((String)args[i]).compareTo("-vv") == 0){
				useLogLevel = Level.FINEST;
			} else if (((String)args[i]).compareTo("-q") == 0){
				useLogLevel = Level.WARNING;
			} else if (((String)args[i]).compareTo("-c")==0) {
				useConfigPath = args[++i];
			}
		}
		
		TLog.log(Level.CONFIG, "++Outputting log information to \"" + new File("diytraffic.log").getAbsolutePath() + "\"" );
		TLog.log(Level.CONFIG, "++Using configuration file \"" + new File(useConfigPath).getAbsolutePath() +"\"");
		
		// Create a new instance of an AirTrafficController object.
		// This serves as the manager, with which you register TrafficSources and TrafficSinks,
		// and register relationships between them.
		// The params (each 5000) represent the interval on which the ATC checks inputs and updates outputs, respectively
		AirTrafficController atc = new AirTrafficController(DEFAULT_INPUTD_INTERVAL*1000, 
				DEFAULT_OUTPUTD_INTERVAL*1000);
		
		TLog.log(Level.CONFIG, "++Using log level of " + useLogLevel);
		TLog.setLogThreshold(useLogLevel);
	
		ArrayList<DIYUKCity> cityList = null;
		try {
			cityList = trafficConfig.configureFromLocalFile(useConfigPath);
			trafficConfig.setDefaultConfigurationFile(useConfigPath);
			
			// Uncomment this if you want to configure from a remote config file
			//cityList = trafficConfig.configureFromRemoteFile(REMOTE_CONFIG_URI);
		} catch (TrafficUKConfigException tce) {
			TLog.log(Level.SEVERE, String.format("Unable to configure from local file %s: %s", useConfigPath, tce.getMessage()));
			TLog.log(Level.SEVERE, "use -help flag for usage information");
			System.exit(-1);
		}
		
		// first time around, you can register all the cities...
		trafficConfig.registerCities(cityList);
		
		
	    Iterator<DIYUKCity> cityIterator=cityList.iterator();
	    while(cityIterator.hasNext()){
	        	DIYUKCity city=cityIterator.next();
	        	TLog.log(Level.INFO, String.format("ATC processing city: %s, Twitter username: %s", city.cityName,  city.twitterName));
	        	try {
	        		processCity(atc, city);
	        	} catch (Exception e) {
	        		TLog.log(Level.SEVERE, e);
	        		TLog.log(Level.WARNING, String.format("Unable to process city %s, ignoring", city.getEncodedAddress()));
	        		continue;
	        	}
	        	
	        }
	       
		
		// Must call this to start the ATC's input and output daemons
		TLog.log(Level.INFO, "Starting ATC Daemon");
	    atc.start();
		
	    /**
	     * This section here just goes into a loop and checks every hour for updates to the configuration file
	     * i.e. if you've addded a new city. Every morning at 3am, it will completely reset state, deleting all the
	     * cities its currently tracking, and reading in the new list of cities from the config file. The reason for
	     * this is simply that all the code hasn't been written to remove cities from the system, only to add new ones.
	     * When all the plumbing is in place to safely remove a city (and all it's dependent sources and sinks), then
	     * we can get rid of this once-a-day-refresh hack.
	     */
		while (true) {
			Thread.sleep(DEFAULT_CONFIG_UPDATE_INTERVAL * 1000); // wakes up 20s
			
			/**
			 * This is a pretty egregious hack :) You can uncomment this line if you dont wan't to check for
			 * updates to the config file every hour..
			 */
			//if (true) continue;
			
			TLog.log(Level.FINEST, "Suspending ATC");
			atc.suspend();
			
			ArrayList<DIYUKCity> configCityList = null;
			try {
				String defaultConfigFile = trafficConfig.getDefaultConfigurationFile();
				if (defaultConfigFile != null) {
					TLog.log(Level.CONFIG, "Reading configuration file at '" + defaultConfigFile + "'");
					configCityList = trafficConfig.configureFromDefaultFile();
				} else 
					continue;
			
			//TLog.log(Level.CONFIG, "Reading configuration file at '" + REMOTE_CONFIG_URI + "'");
			//configCityList = trafficConfig.configureFromRemoteFile(REMOTE_CONFIG_URI);
			
			} catch (TrafficUKConfigException tce) {
				TLog.log(Level.WARNING, String.format("Unable to configure from remote file %s: %s", REMOTE_CONFIG_URI, tce.getMessage()));
				continue;
			}
			
			
			Calendar cal = GregorianCalendar.getInstance();
			int hour = cal.get(Calendar.HOUR_OF_DAY);
			//System.out.println("HOUR HERE IS " + hour);
			
			//TODO refactor this code - so much repition here! 
			if ( hour == 3 ) {
				//i.e. if it's three o'clock in the morning
				TLog.log(Level.CONFIG, "*********Resetting state*********");
				atc.clear();
				trafficConfig.clearRegisteredCities();
				
				Iterator<DIYUKCity> newCityIterator = configCityList.iterator();
				while (newCityIterator.hasNext()) {
					DIYUKCity next_city = newCityIterator.next();
					try {
						processCity(atc, next_city);
					} catch (Exception e) {
						TLog.log(Level.SEVERE, e);
						TLog.log(Level.SEVERE, String.format("Unable to process city %s, ignoring", next_city.getEncodedAddress()));
						continue;
					}
					trafficConfig.registerCity(next_city);
				}
				
			} else {
				TLog.log(Level.CONFIG, "*********Looking for new configuration info*********");
				ArrayList<DIYUKCity> newCitiesOnly = trafficConfig.determineNewCities(configCityList);
				Iterator<DIYUKCity> newCitiesIterator = newCitiesOnly.iterator();
				TLog.log(Level.CONFIG, "Found " + newCitiesOnly.size() + " new cities.");
				while (newCitiesIterator.hasNext()) {
					DIYUKCity next_city = newCitiesIterator.next();
					TLog.log(Level.CONFIG, next_city.cityName);
					try {
						processCity(atc, next_city);
					} catch (Exception e) {
						TLog.log(Level.SEVERE, e);
						TLog.log(Level.SEVERE, String.format("Unable to process city %s, ignoring", next_city.getEncodedAddress()));
						continue;
					}
					trafficConfig.registerCity(next_city);
				}
			}
			
			
			TLog.log(Level.FINEST, "Reviving ATC");
			atc.start();
			TLog.log(Level.FINEST, "Done reviving");
		}
	}
	
	public static void processCity(AirTrafficController atc, DIYUKCity city) throws Exception {
		/** Here is some code for registering a BlueGhostTrafficSource a city and state for
		 *  traffic updates.
	     */
		
    	
		// This just returns a String which replaces any spaces in the names with a "-" and also puts
		// a "_" between elements. It's an arbitrary standard way for representing address information in
		// a single String with no spaces.
		String encoded_address = city.getEncodedAddress();
		
		BlueGhostTrafficSource bgts = new BlueGhostTrafficSource(city.cityName);
		
		// Set the frequency (in milliseconds) with which the Yahoo! traffic source should update. 
		bgts.setUpdateFrequency(DEFAULT_TRAFFIC_CHECK_INTERVAL * 1000);
			
		// Register the traffic source with the atc, so it will be checked for updates on the appropriate interval
		if (!atc.isTrafficSourceRegistered(bgts))
			atc.registerTrafficSource(bgts);
		TLog.log(Level.FINER, "Registered BlueGhost Traffic Source '" + bgts.getName()+ "' with ATC");
		TLog.log(Level.FINER, String.format("Will check %s on interval of %s minutes", city.cityName, bgts.getUpdateFrequency()/1000/60));
		
		/** This next bit of code creates a TwitterStatusSink, which takes TrafficUpdates and posts them as 
		 * status messages to a particular Twitter account. The config file contains info about a twitter username,
		 * a twitter password and also a number called maxUpdates representing a rate limit. The rate limit is a value (int) 
		 * which represents how many times this sink will be allowed to post an
		 * update in the span of a minute. This is to prevent situations where a sink might update constantly, annoying
		 * the heck out of its users. Here, we're saying that this TrafficSink can update at most three times a minute.
		 * Any TrafficUpdates that happen within the minute but are outside the limit are essentially lost.
		 */
				
		// check to see if a TwitterStatusSink representing 'diy_chi_test' already exists
		TwitterStatusSink tss = TwitterStatusSink.getInstanceByUsername(city.twitterName);
		if ( tss == null) {
			// if one doesn't already exist, create one linked to a particular Twitter account
			tss = TwitterStatusSink.createInstance(city.twitterName, city.twitterPassword, DEFAULT_STATUS_MAX_UPDATES);
		}
		//register this sink with the ATC
		if (!atc.isTrafficSinkRegistered(tss))
			atc.registerTrafficSink(tss);
		TLog.log(Level.FINER, "Registered Twitter sink" + tss.getName()+ " with ATC");
		TLog.log(Level.FINER, "Maximum allowable number of status updates per minute is " + DEFAULT_STATUS_MAX_UPDATES);
		
		// Establish a relationship between a particular sink and source. Any updates from the source will be passed
		// on to the source, and handled within the allowances of defined rateLimits.
		atc.establishPipe(bgts, tss);
		
		
		
		/** In this next example, we're going to set up a TwitterDMSource. This represents a
		 * TrafficSource, which, instead of listening to actual traffic, listens to direct messages
		 * coming in on a particular Twitter account. Based on the content of the direct message, it
		 * will perform a query on recent updates on that Twitter Account, and then send a series of
		 * messages back to the sender of the direct message with any statuses which match the query
		 */
		
	
		// Create a TwitterDMSource, if one doesn't already exist for this Twitter account
		TwitterDMSource dm_source = TwitterDMSource.getInstanceByName(city.twitterName);
		if (dm_source == null)
			// The last argument here represents the update frequency (in SECONDS)  for this source.
			// It means that it should check for new messages every n seconds
			dm_source = TwitterDMSource.createInstance(city.twitterName, city.twitterPassword, DEFAULT_DM_CHECK_INTERVAL);
		
		if (!atc.isTrafficSourceRegistered(dm_source))	
			atc.registerTrafficSource(dm_source);
		TLog.log(Level.FINER, "Registered Twitter Direct Message Source" + dm_source.getName()+ " with ATC");
		TLog.log(Level.FINER, "Will check for Direct Message queries every 30 seconds");
		
		// Create a TwitterDMSink. This is a TrafficSink that knows how to handle updates coming
		// from a TwitterDMSource. The unique thing about those updates is that they have some
		// data attached to them which specify a recipient for a direct message (see 
		// TwitterSink.DM_RECIPIENT
		TwitterDMSink dm_sink = TwitterDMSink.getInstanceByUsername(city.twitterName);
		if (dm_sink == null)
			// the DEFAULT_DM_MAX_UPDATES here represents the rateLimit for this sink: 100 updates per minute
			// this is a bit tricky, because there's unfortunately no way to specify a max
			// number of updates per dm recipient, just for the sink as a whole. Hence, the high
			// rate limit. 
			dm_sink = TwitterDMSink.createInstance(city.twitterName, city.twitterPassword, DEFAULT_DM_MAX_UPDATES);
		
		// register the TwitterDMSink with the ATC
		if (!atc.isTrafficSinkRegistered(dm_sink))
			atc.registerTrafficSink(dm_sink);
		
		TLog.log(Level.FINER, "Registered Twitter Direct Message Sink" + dm_sink.getName()+ " with ATC");
		TLog.log(Level.FINER, "Maximum allowable number of direct messages sent per minute is 100");
		
		// Establish a relationship between the TwitterDMSource and TwitterDMSink, so that any updates
		// coming from the source will be sent to the sink
		atc.establishPipe(dm_source, dm_sink);
	}
}
